/**
 * Copyright 2019 吉鼎科技.
 *
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.support.scripting.cmd;

import cn.easyplatform.EasyPlatformWithLabelKeyException;
import cn.easyplatform.FieldNotFoundException;
import cn.easyplatform.ScriptEvalExitException;
import cn.easyplatform.ScriptRuntimeException;
import cn.easyplatform.contexts.ListContext;
import cn.easyplatform.contexts.RecordContext;
import cn.easyplatform.contexts.WorkflowContext;
import cn.easyplatform.dos.EnvDo;
import cn.easyplatform.dos.FieldDo;
import cn.easyplatform.dos.Record;
import cn.easyplatform.dos.UserDo;
import cn.easyplatform.entities.beans.table.TableBean;
import cn.easyplatform.entities.beans.table.TableField;
import cn.easyplatform.i18n.I18N;
import cn.easyplatform.interceptor.CommandContext;
import cn.easyplatform.lang.Nums;
import cn.easyplatform.lang.Strings;
import cn.easyplatform.spi.extension.ApplicationService;
import cn.easyplatform.spi.extension.ProjectContext;
import cn.easyplatform.spi.extension.RuntimeContext;
import cn.easyplatform.support.scripting.RhinoScriptable;
import cn.easyplatform.support.scripting.ScriptCmdContext;
import cn.easyplatform.support.sql.SqlParser;
import cn.easyplatform.support.word.ReserveWordFactory;
import cn.easyplatform.type.DeviceType;
import cn.easyplatform.type.FieldType;
import cn.easyplatform.type.IResponseMessage;
import cn.easyplatform.type.ScopeType;
import cn.easyplatform.util.MessageUtils;
import cn.easyplatform.util.RuntimeUtils;
import org.mozilla.javascript.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.*;

/**
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a> <br/>
 * @since 2.0.0 <br/>
 */
public class MainCmd implements ScriptCmdContext, RuntimeContext,
        Serializable {

    /**
     *
     */
    private static final long serialVersionUID = 1L;

    private static final Logger log = LoggerFactory.getLogger(MainCmd.class);

    private transient CommandContext cc;

    private RhinoScriptable scope;

    private WorkflowContext ctx;

    private RecordContext target;

    private RecordContext source;

    private transient DbCmd dbCmd;

    public MainCmd(RhinoScriptable scope, CommandContext cc,
                   WorkflowContext ctx) {
        this.scope = scope;
        this.cc = cc;
        this.ctx = ctx;
        this.dbCmd = new DbCmd(this);
    }

    public boolean exists(String name, char c) {
        if (c == '@' && source == null)
            throw new FieldNotFoundException("entity.table.variable.not.found",
                    name);
        if (c == '@')
            return source.exists(name);
        return target.exists(name);
    }

    public void exit(Object code) {
        if (code == null)
            code = "<null>";
        throw new ScriptEvalExitException(target, code.toString());
    }

    @Override
    public void debug(Object obj) {
        cc.send(obj);
    }

    @Override
    public void log(Object obj) {
        log.info(obj == null ? "null" : obj.toString());
    }

    @Override
    public void error(Object obj, Exception e) {
        log.error(obj == null ? "null" : obj.toString(), e);
    }

    public FieldDo createVariable(String name, String type, String scope,
                                  int mode) {
        FieldDo var = new FieldDo(FieldType.valueOf(type.toUpperCase()));
        var.setName(name);
        if (scope != null)
            var.setScope(ScopeType.valueOf(scope.toUpperCase()));
        else
            var.setScope(ScopeType.PRIVATE);
        var.setShareModel(mode);
        target.setVariable(var);
        return var;
    }

    public Object get$(String name) {
        if (ctx.getHost() != null)
            return ctx.getHost().getValue(name);
        return null;
    }

    public void set$(String name, Object value) {
        if (ctx.getHost() != null)
            ctx.getHost().setValue(name, value);
    }

    public Object get0$(String name) {
        if (ctx.getParentId() != null) {
            WorkflowContext cx = cc.getWorkflowContext(ctx.getParentId());
            if (cx != null)
                return cx.getRecord().getValue(name);
        }
        return null;
    }

    public void set0$(String name, Object value) {
        if (ctx.getParentId() != null) {
            WorkflowContext cx = cc.getWorkflowContext(ctx.getParentId());
            if (cx != null)
                cx.getRecord().setValue(name, value);
        }
    }

    public RecordContext[] list$(String listId, boolean sel) {
        if (ctx.getParentId() != null) {
            WorkflowContext cx = cc.getWorkflowContext(ctx.getParentId());
            if (cx != null) {
                ListContext sc = cx.getList(listId);
                if (sc != null) {
                    List<RecordContext> records = new ArrayList<RecordContext>();
                    for (RecordContext rc : sc.getRecords()) {
                        if (rc.getParameterAsChar("814") != 'D') {
                            if (sel && !rc.getParameterAsBoolean("853"))
                                continue;
                            records.add(rc);
                        }
                    }
                    RecordContext[] ar = new RecordContext[records.size()];
                    records.toArray(ar);
                    return ar;
                }
            }
        }
        return null;
    }

    public void setVariable(String name, Object value, char c, String type) {
        if ((c == '@' || type.equals("@")) && source == null)
            throw new FieldNotFoundException("entity.table.variable.not.found",
                    name);
        value = RuntimeUtils.castTo(value);
        if (name.length() == 3 && name.compareTo("700") >= 0
                && name.compareTo("899") < 0) {
            if (c == '$')
                target.setParameter(name, value);
            else
                source.setParameter(name, value);
        } else {
            FieldDo fd = null;
            if (c == '$')
                fd = target.getField(name);
            else
                fd = source.getField(name);
            if (type.equals("") || type.equals("value"))
                fd.setValue(value);
            else {
                if (type.equals("name"))
                    fd.setName(value == null ? "" : value.toString());
                else if (type.equals("len"))
                    fd.setLength(Nums.toInt(value, 0));
                else if (type.equals("decimal"))
                    fd.setDecimal(Nums.toInt(value, 0));
                else if (type.equals("scope"))
                    fd.setScope(ScopeType.valueOf(value.toString()));
                else if (type.equals("acc"))
                    fd.setAcc(value == null ? "" : value.toString());
                else if (type.equals("mode"))
                    fd.setShareModel(Nums.toInt(value, 0));
                else if (type.equals("$") || type.equals("@")) {
                    if (fd.getValue() != null)
                        setVariable(fd.getValue().toString(), value, type.charAt(0), "");
                }
            }
        }
    }

    public Object getVariable(String name, char c, String type) {
        if ((c == '@' || type.equals("@")) && source == null)
            throw new FieldNotFoundException("entity.table.variable.not.found",
                    name);
        if (name.length() == 3 && name.compareTo("700") >= 0
                && name.compareTo("899") < 0) {
            if (c == '$')
                return target.getParameter(name);
            else
                return source.getParameter(name);
        }
        FieldDo fd = null;
        if (c == '$')
            fd = target.getField(name);
        else {
            if ("IMPORT".equals(source.getParameterAsString("759"))) {
                fd = source.getFieldQuietly(name);
                if (fd == null)
                    return null;
            } else
                fd = source.getField(name);
        }
        if (type.equals("") || type.equals("value"))
            return fd.getValue();
        else if (type.equals("type"))
            return fd.getType().toString();
        else if (type.equals("name"))
            return fd.getName();
        else if (type.equals("len"))
            return fd.getLength();
        else if (type.equals("decimal"))
            return fd.getDecimal();
        else if (type.equals("scope"))
            return fd.getScope().toString();
        else if (type.equals("acc"))
            return fd.getAcc();
        else if (type.equals("mode"))
            return fd.getShareModel();
        else if (type.equals("empty")) {
            return fd.isEmpty();
        } else if (type.equals("length")) {
            if (fd.getValue() instanceof Object[]) {
                return ((Object[]) fd.getValue()).length;
            } else
                return fd.getValue() == null ? 0 : String
                        .valueOf(fd.getValue()).length();
        } else if (type.equals("$") || type.equals("@")) {// 获取值中值
            if (fd.getValue() != null)
                return getVariable(fd.getValue().toString(), type.charAt(0), "");
            return null;
        } else
            return fd.getValue();
    }

    @Override
    public void setValue(String name, Object value) {
        setVariable(name, value, '$', "");
    }

    @Override
    public Object getValue(String name) {
        if (scope.containsKey(name)) {
            return scope.get(name);
        } else {
            FieldDo tf = target.getField(name);
            if (tf == null)
                throw new RuntimeException(I18N.getLabel("entity.table.variable.not.found", name));
            return tf.getValue();
        }
    }

    public Object getReserveWord(String name) {
        return ReserveWordFactory.getReserveWord(name);
    }

    /**
     * 从数据库直接获取数据
     *
     * @param sql
     * @param expr
     */
    public void load(String sql, String expr) {
        if (sql == null) {
            TableBean tb = cc.getEntity(target.getParameterAsString("833"));
            if (tb.getKey() != null && !tb.getKey().isEmpty()) {
                Object[] keys = new Object[tb.getKey().size()];
                int index = 0;
                for (String name : tb.getKey()) {
                    FieldDo field = target.getFieldQuietly(name);
                    if (field == null) {
                        keys = null;
                        break;
                    }
                    keys[index++] = field.getValue();
                }
                if (keys != null)
                    target.setData(RuntimeUtils.getTableRecord(cc, tb, keys));
            }
        } else {
            boolean isCustom = sql.toLowerCase().startsWith("select");
            TableBean table = null;
            if (!isCustom) {
                StringBuilder sb = new StringBuilder("select ");
                table = cc.getEntity(target.getParameterAsString("833"));
                Iterator<TableField> itr = table.getFields().iterator();
                while (itr.hasNext()) {
                    sb.append(itr.next().getName());
                    if (itr.hasNext())
                        sb.append(",");
                }
                sb.append(" from ").append(table.getId()).append(" where ")
                        .append(sql);
                sql = sb.toString();
                sb = null;
            }
            SqlParser<FieldDo> sp = RuntimeUtils.createSqlParser(FieldDo.class);
            sql = sp.parse(sql, scope, source, target);
            FieldDo[] fields = cc.getBizDao().selectOne(sql, sp.getParams());
            if (log.isDebugEnabled())
                log.debug("load({}),{}", sql, sp.getParams());
            if (fields == null)
                throw new EasyPlatformWithLabelKeyException(
                        "context.record.not.found", "load()");
            if (isCustom) {
                if (target.getData() == null) {
                    Record data = new Record();
                    for (FieldDo fd : fields)
                        data.set(fd);
                    target.setData(data);
                } else if (Strings.isBlank(expr)) {
                    Set<String> columnNames = target.getData().getColumnNames();
                    for (FieldDo field : fields) {
                        if (columnNames.contains(field.getName()))
                            target.setValue(field.getName(), field.getValue());
                    }
                } else {
                    String code = RuntimeUtils.eval(cc, expr, target, fields);
                    if (!code.equals("0000"))
                        throw new ScriptRuntimeException(code, cc.getMessage(
                                code, target));
                }
            } else {
                target.setData(RuntimeUtils.createRecord(cc, table, false));
                for (FieldDo field : fields)
                    target.setValue(field.getName(), field.getValue());
                if (!Strings.isBlank(expr)) {
                    String code = RuntimeUtils.eval(cc, expr, target, fields);
                    if (!code.equals("0000"))
                        throw new ScriptRuntimeException(code, cc.getMessage(
                                code, target));
                }
            }
        }
    }

    /**
     * 从指定的层获取数据
     *
     * @param layer
     * @param expr
     */
    public void loadFrom(int layer, String expr) {
        RecordContext rc = ctx.getRecordContext(layer);
        if (rc == null)
            throw new EasyPlatformWithLabelKeyException("task.not.found", layer);
        load(rc, expr);
    }

    /**
     * 从指定的层的子功能获取数据
     *
     * @param layer
     * @param taskId
     * @param expr
     */
    public void loadFrom(int layer, String taskId, String expr) {
        String id = ctx.getTask(layer, taskId);
        if (id == null)
            throw new EasyPlatformWithLabelKeyException(
                    "task.not.found.from.layer", layer, taskId);
        WorkflowContext wc = cc.getWorkflowContext(id);
        if (wc == null)
            throw new EasyPlatformWithLabelKeyException(
                    "task.not.found.from.layer", layer, taskId);
        RecordContext rc = wc.getRecord();
        load(rc, expr);
    }

    public String getSerial(String name, int length) {
        Long id = cc.getIdGenerator().getNextId(name);
        String str = id.toString();
        if (length > 0) {
            if (str.length() < length) {
                StringBuilder sb = new StringBuilder(length);
                sb.append(str);
                while (sb.length() < length)
                    sb.insert(0, '0');
                str = sb.toString();
            }
        }
        return str;
    }

    public void resetSerial(String name) {
        cc.getIdGenerator().reset(name, 0);
    }

    public void resetSerial(String name, long value) {
        cc.getIdGenerator().reset(name, value);
    }

    public void deleteSerial(String name) {
        cc.getIdGenerator().delete(name);
    }

    public void invoke(String expr, String[] args) {
        if (args.length > 0) {
            if (args.length == 1)
                args = args[0].split("\\,");
            RecordContext rc = target.create();
            Record data = rc.getData();
            for (int index = 1; index <= args.length; index++) {
                String name = args[index - 1].trim();
                FieldDo fd = null;
                if (!name.equals(""))
                    fd = target.getField(name).clone();
                else
                    fd = new FieldDo(FieldType.INT);
                fd.setName(String.valueOf(index));
                data.set(fd);
            }
            String code = RuntimeUtils.eval(cc, expr, rc);
            if (!code.equals("0000"))
                throw new ScriptRuntimeException(code, cc.getMessage(code, rc));
            for (int index = 1; index <= args.length; index++) {
                String name = args[index - 1].trim();
                if (!name.equals("")) {
                    FieldDo fd = target.getField(name);
                    fd.setValue(data.get(String.valueOf(index)).getValue());
                }
            }
        } else {
            String code = RuntimeUtils.eval(cc, expr, target);
            if (!code.equals("0000"))
                throw new ScriptRuntimeException(code, cc.getMessage(code,
                        target));
        }
    }

    /**
     * 获取指定层的列表
     *
     * @param layer
     * @param id
     * @return
     */
    public ListCmd getList(int layer, String id) {
        ListContext lc = ctx.getList(layer, id);
        if (lc == null)
            throw new EasyPlatformWithLabelKeyException(
                    "datalist.not.found.from.layer", layer, id);
        return new ListCmd(ctx, lc, this);
    }

    /**
     * @param layer
     * @param listId
     * @return
     */
    public List<Map<String, Object>> toMapList(int layer, String listId) {
        ListContext lc = ctx.getList(layer, listId);
        if (lc == null)
            throw new EasyPlatformWithLabelKeyException(
                    "datalist.not.found.from.layer", layer, listId);
        return lc.toMapList(layer, false);
    }

    /**
     * @param layer
     * @param listId
     * @return
     */
    public List<Map<String, Object>> toSelectedMapList(int layer, String listId) {
        ListContext lc = ctx.getList(layer, listId);
        if (lc == null)
            throw new EasyPlatformWithLabelKeyException(
                    "datalist.not.found.from.layer", layer, listId);
        return lc.toMapList(layer, true);
    }

    // 以下并不显示给引擎
    public void setData(RecordContext... rcs) {
        if (rcs.length == 1)
            target = rcs[0];
        else if (rcs.length == 2) {
            target = rcs[1];
            source = rcs[0];
        } else if (rcs.length == 3) {
            target = rcs[1];
            source = rcs[0];
            scope.setVariable("root", rcs[2]);
        }
    }

    public DbCmd getDbCmd() {
        return dbCmd;
    }

    public UtilCmd getUtilCmd() {
        return new UtilCmd(this);
    }

    public DateCmd getDateCmd() {
        return new DateCmd(this);
    }

    public BpmCmd getBpmCmd() {
        return new BpmCmd(this);
    }

    public AppCmd getAppCmd() {
        return new AppCmd(this);
    }

    public Object getCustomCmd(String name) {
        Object cmd = cc.getProjectService()
                .getCommand(name);
        if (cmd == null) {
            ApplicationService as = cc.getEngineConfiguration().getService(name);
            if (as == null)
                as = cc.getProjectService().getService(name);
            if (as == null || !as.isCallable())
                throw new EasyPlatformWithLabelKeyException(
                        "script.engine.cmd.not.found", name);
            return as;
        }
        // 动态注入系统变量
        if (!RuntimeUtils.injectObject(cmd, cc.getProjectService(), this, ProjectContext.class, RuntimeContext.class))
            throw new EasyPlatformWithLabelKeyException("easyplatform.spi.error2", name);
        return cmd;
    }

    public FieldDo[] getFields() {
        if (target.getData() != null) {
            FieldDo[] fields = new FieldDo[target.getData().getColumnCount()];
            target.getData().getData().toArray(fields);
            return fields;
        }
        return null;
    }

    public boolean isEmpty(Object obj) {
        if (obj == null)
            return true;
        if (obj instanceof Object[] && ((Object[]) obj).length == 0)
            return true;
        if (Strings.isBlank(obj.toString()))
            return true;
        return false;
    }

    public boolean isDefine(String name) {
        return scope.containsKey(name);
    }

    public boolean isEquals(Object src, Object target) {
        if (src == null && target == null)
            return true;
        if (src == null || target == null)
            return false;
        if (src.equals(target))
            return true;
        if (!src.getClass().isAssignableFrom(target.getClass())
                && !target.getClass().isAssignableFrom(src.getClass()))
            return false;
        if (src instanceof Date) {
            Calendar cal1 = Calendar.getInstance();
            cal1.setTime((Date) src);
            Calendar cal2 = Calendar.getInstance();
            cal2.setTime((Date) target);
            if (cal1.get(Calendar.YEAR) == cal2.get(Calendar.YEAR)
                    && cal1.get(Calendar.MONTH) == cal2.get(Calendar.MONTH)
                    && cal1.get(Calendar.DATE) == cal2.get(Calendar.DATE))
                return true;
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    public void go(final String taskId, final String code) {
        EnvDo env = cc.getEnv();
        UserDo user = cc.getUser();
        DeviceType deviceType = env.getDeviceType();
        env.setDeviceType(DeviceType.JOB);
        user.setDeviceType(env.getDeviceType());
        RecordContext tmp = ctx.getRecord();
        ctx.setRecord(target);
        try {
            IResponseMessage<?> resp = cc.getEngineConfiguration().getTaskExecuter(cc).execute(new cn.easyplatform.contexts.ExecuteContext(cc, taskId, code));
            if (!resp.isSuccess())
                throw new ScriptRuntimeException(resp.getCode(), (String) resp.getBody());
        } catch (Exception ex) {
            if (ex instanceof ScriptRuntimeException)
                throw (ScriptRuntimeException) ex;
            else if (ex instanceof EasyPlatformWithLabelKeyException)
                throw (EasyPlatformWithLabelKeyException) ex;
            throw new EasyPlatformWithLabelKeyException(
                    "script.engine.go.error", MessageUtils.getMessage(ex, log)
                    .getBody());
        } finally {
            cc.setWorkflowContext(ctx);
            ctx.setRecord(tmp);
            env.setDeviceType(deviceType);
            user.setDeviceType(deviceType);
        }
    }

    /**
     * 添加要执行的任务
     *
     * @param taskId
     * @param code
     */
    public void appendTask(final String taskId, final String code) {
        RecordContext tmp = ctx.getRecord();
        ctx.setRecord(target);
        try {
            cc.appendTask(taskId, code);
        } finally {
            ctx.setRecord(tmp);
        }
    }

    /**
     * 开始批量执行多个任务
     *
     * @param await 是否等待
     */
    public void executeBatch(boolean await) {
        cc.executeBatch(await);
    }

    public String i18n(String name) {
        return cc.getLabel(name);
    }

    // /////////////////以下为系统调用////////////////////////
    public void setEngine(CommandContext cc) {
        this.cc = null;
        this.cc = cc;
    }

    @Override
    public CommandContext getCommandContext() {
        return cc;
    }

    @Override
    public RecordContext getTarget() {
        return target;
    }

    @Override
    public RecordContext getSource() {
        return source;
    }

    @Override
    public void setTarget(RecordContext target) {
        this.target = target;
    }

    @Override
    public void setSource(RecordContext source) {
        this.source = source;
    }

    @Override
    public WorkflowContext getWorkflowContext() {
        return ctx;
    }

    @Override
    public Object javaToJS(Object value) {
        return Context.javaToJS(value, scope);
    }

    @Override
    public Object jsToJava(Object value, Class<?> desiredType) {
        return Context.jsToJava(value, desiredType);
    }

    @Override
    public RhinoScriptable getScope() {
        return scope;
    }

    @Override
    public void setScope(RhinoScriptable scope) {
        this.scope = scope;
    }

    public Object getObject(String name) {
        FieldDo fd = target.getFieldQuietly(name);
        if (fd != null)
            return fd.getValue();
        return scope.get(name);
    }

    private void load(RecordContext rc, String expr) {
        if (Strings.isBlank(expr)) {
            Set<String> columnNames = target.getData().getColumnNames();
            for (FieldDo field : rc.getRecordFieldValues()) {
                if (columnNames.contains(field.getName()))
                    target.setValue(field.getName(), field.getValue());
            }
        } else {
            String code = RuntimeUtils.eval(cc, expr, rc, target);
            if (!code.equals("0000"))
                throw new ScriptRuntimeException(code, cc.getMessage(code,
                        target));
        }
    }


    //序列化
    private static int bitIndexCounter = 0;
    private static final int SCORE_BIT_MASK = 1 << bitIndexCounter++;
    private static final int CTX_BIT_MASK = 1 << bitIndexCounter++;
    private static final int TARGET_BIT_MASK = 1 << bitIndexCounter++;
    private static final int SOURCE_BIT_MASK = 1 << bitIndexCounter++;

    private short getAlteredFieldsBitMask() {
        int bitMask = 0;
        bitMask = scope != null ? bitMask | SCORE_BIT_MASK : bitMask;
        bitMask = ctx != null ? bitMask | CTX_BIT_MASK : bitMask;
        bitMask = target != null ? bitMask | TARGET_BIT_MASK : bitMask;
        bitMask = source != null ? bitMask | SOURCE_BIT_MASK : bitMask;
        return (short) bitMask;
    }

    private static boolean isFieldPresent(short bitMask, int fieldBitMask) {
        return (bitMask & fieldBitMask) != 0;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        short alteredFieldsBitMask = getAlteredFieldsBitMask();
        out.writeShort(alteredFieldsBitMask);
        if (scope != null)
            out.writeObject(scope);
        if (ctx != null)
            out.writeObject(ctx);
        if (target != null)
            out.writeObject(target);
        if (source != null)
            out.writeObject(source);
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        short bitMask = in.readShort();
        if (isFieldPresent(bitMask, SCORE_BIT_MASK)) {
            this.scope = (RhinoScriptable) in.readObject();
        }
        if (isFieldPresent(bitMask, CTX_BIT_MASK)) {
            this.ctx = (WorkflowContext) in.readObject();
        }
        if (isFieldPresent(bitMask, TARGET_BIT_MASK)) {
            this.target = (RecordContext) in.readObject();
        }
        if (isFieldPresent(bitMask, SOURCE_BIT_MASK)) {
            this.source = (RecordContext) in.readObject();
        }
        this.dbCmd = new DbCmd(this);
    }
}
